﻿namespace EyeOpen.Mapping.WcfContract
{
    using System;
    using System.CodeDom;
    using System.CodeDom.Compiler;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Text;

    public class WcfContractGenerator : IMapper<CodeNamespace>
    {
        #region Fields
        private static readonly CodeGeneratorOptions codeGenerateOptions = GetCodeGeneratorOptions();
        private IList<Type> types;
        private CodeDomProvider codeDomProvider;
        private Assembly assembly;
        private string languageExtension;
        private bool haveToGenerateAssembly = false;
        private string outputFolder;
        private string originalNamespace;
        private string replaceNamespace;
        private bool completlyReplaceNamespace;
        private CompilerErrorCollection compilerErrors;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="ContractGenerator"/> class using C# as default language.
        /// </summary>
        public WcfContractGenerator()
        {
            this.languageExtension = "cs";
        }

        public WcfContractGenerator(string languageExtension)
        {
            this.languageExtension = languageExtension;
        }

        public WcfContractGenerator(string languageExtension, bool haveToGenerateAssembly)
            : this(languageExtension)
        {
            this.haveToGenerateAssembly = haveToGenerateAssembly;
        }
        #endregion

        #region Properties
        public Assembly Assembly
        {
            get
            {
                return this.assembly;
            }
        }

        public CompilerErrorCollection CompilerErrors
        {
            get { return this.compilerErrors; }
        }

        public string OutputFolder
        {
            get { return this.outputFolder; }
            set { this.outputFolder = value; }
        }

        public bool CompletlyReplaceNamespace
        {
            get { return this.completlyReplaceNamespace; }
            set { this.completlyReplaceNamespace = value; }
        }

        public string OriginalNamespace
        {
            get { return this.originalNamespace; }
            set { this.originalNamespace = value; }
        }

        public string ReplaceNamespace
        {
            get { return this.replaceNamespace; }
            set { this.replaceNamespace = value; }
        }

        private CodeDomProvider CodeDomProvider
        {
            get
            {
                if (this.codeDomProvider == null)
                {
                    string language = CodeDomProvider.GetLanguageFromExtension(this.languageExtension);
                    this.codeDomProvider = CodeDomProvider.CreateProvider(language);
                }

                return this.codeDomProvider;
            }
        }
        #endregion
        
        public string GetSourceCodeFromCodeNameSpace(CodeNamespace codeNamespace)
        {
            StringBuilder builder = new StringBuilder();
            using (StringWriter writer = new StringWriter(builder))
            {
                CodeDomProvider.GenerateCodeFromNamespace(codeNamespace, writer, codeGenerateOptions);
            }

            string dataContractBody = builder.ToString();

            ////WCF attributes adjuctment C#
            dataContractBody = dataContractBody.Replace("[DataContract()]", "[DataMember]");
            dataContractBody = dataContractBody.Replace("[DataMember()]", "[DataMember]");

            return dataContractBody;
        }

        public IDictionary<Type, CodeNamespace> GenerateMappings(Assembly assembly)
        {
            this.types = BaseMapper.GetTypes(assembly);
            List<Type> typesList = this.types.ToList();

            Dictionary<Type, CodeNamespace> mappings = new Dictionary<Type, CodeNamespace>();

            CodeCompileUnit codeCompileUnit = new CodeCompileUnit();

            string namespaceName;
            if (this.CompletlyReplaceNamespace)
            {
                namespaceName = this.ReplaceNamespace;
            }
            else
            {
                namespaceName = assembly.GetName().Name.Replace(this.OriginalNamespace, this.ReplaceNamespace);
            }

            foreach (Type type in this.types)
            {
                CodeNamespace codeNamespace = this.GenerateMapping(type, namespaceName);                
                codeCompileUnit.Namespaces.Add(codeNamespace);
                mappings.Add(type, codeNamespace);
            }

            CompilerParameters compilerParameters = new CompilerParameters();
            compilerParameters.GenerateExecutable = false;
            compilerParameters.IncludeDebugInformation = false;

            ////TODO(MM): replace fix path to %Path%
            codeCompileUnit.ReferencedAssemblies.Add(@"c:\windows\Microsoft.NET\Framework\v2.0.50727\System.dll");
            codeCompileUnit.ReferencedAssemblies.Add(@"c:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\System.Runtime.Serialization.dll");
            codeCompileUnit.ReferencedAssemblies.Add(@"c:\Program Files\Reference Assemblies\Microsoft\Framework\v3.5\System.Core.dll");

            CodeAttributeDeclaration assemblyTitle = new CodeAttributeDeclaration("AssemblyTitle");

            compilerParameters.GenerateInMemory = this.haveToGenerateAssembly;

            if (this.haveToGenerateAssembly)
            {
                if (string.IsNullOrEmpty(this.OutputFolder))
                {
                    throw new ApplicationException("If HaveToGenerateAssembly is set on true must specify a valid output folder.");
                }

                if (!Directory.Exists(this.OutputFolder))
                {
                    throw new ApplicationException(string.Format("Cannot find the specified output folder \"{0}\".", this.OutputFolder));
                }

                string compiledAssemblyPath = Path.Combine(this.OutputFolder, namespaceName);
                compiledAssemblyPath += ".dll";
                compilerParameters.OutputAssembly = compiledAssemblyPath;
            }

            CompilerResults results = CodeDomProvider.CompileAssemblyFromDom(compilerParameters, codeCompileUnit);

            if (results.Errors.HasErrors)
            {
                this.assembly = null;
                this.compilerErrors = results.Errors;
            }
            else 
            {
                this.assembly = results.CompiledAssembly;
                this.compilerErrors = null;
            }            

            return mappings;
        }

        private static CodeGeneratorOptions GetCodeGeneratorOptions()
        {
            CodeGeneratorOptions generatorOptions = new CodeGeneratorOptions();
            generatorOptions.BlankLinesBetweenMembers = true;
            generatorOptions.ElseOnClosing = true;
            generatorOptions.BracingStyle = "C";
            return generatorOptions;
        }

        private CodeNamespace GenerateMapping(Type type, string namespaceName)
        {
            CodeNamespaceImportCollection namespaces = new CodeNamespaceImportCollection();

            CodeTypeDeclaration dataContract = new CodeTypeDeclaration(type.Name);
            dataContract.IsClass = true;
            dataContract.IsPartial = true;
            dataContract.CustomAttributes.Add(new CodeAttributeDeclaration("DataContract"));

            HashSet<string> namespacesHashset = new HashSet<string>();
            
            PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);

            PropertyComparer propertyComparer = new PropertyComparer();

            List<PropertyInfo> propertiesFiltered =
            properties.ToList()
            .OrderBy<PropertyInfo, PropertyInfo>(property => property, propertyComparer)
            .ToList();

            foreach (PropertyInfo property in propertiesFiltered)
            {
                if (property.Name == "Key")
                {
                    continue;
                }

                CodeMemberField codeMemberField = new CodeMemberField();
                if (property.PropertyType.Namespace.StartsWith("System."))
                {
                    namespacesHashset.Add(property.PropertyType.Namespace);
                }

                string propertyName;
                if (!property.PropertyType.IsGenericType)
                {
                    propertyName = property.PropertyType.Name;
                }
                else 
                {
                    string[] genericsComponents = property.PropertyType.Name.Split('`');
                    Type[] genericsArguments = property.PropertyType.GetGenericArguments();
                    propertyName = genericsComponents[0] + "<";
                    foreach (Type genericArgument in genericsArguments) 
                    {
                        propertyName += string.Format("{0}, ", genericArgument.Name);
                    }

                    propertyName = propertyName.TrimEnd(", ".ToCharArray());
                    propertyName += ">";
                }                

                codeMemberField.Type = new CodeTypeReference(propertyName);
                codeMemberField.Name = this.GetFieldNameFromProperty(this.languageExtension, property);

                CodeMemberProperty codeMemberProperty = new CodeMemberProperty();
                codeMemberProperty.Name = property.Name;
                codeMemberProperty.Type = codeMemberField.Type;
                codeMemberProperty.HasGet = true;
                codeMemberProperty.HasSet = true;
                codeMemberProperty.Attributes =
                    MemberAttributes.Public |
                    MemberAttributes.Final;

                CodeSnippetStatement code = null;
                if (this.languageExtension == "cs") 
                { 
                    code = new CodeSnippetStatement(string.Format("{1}return this.{0};", codeMemberField.Name, " ".PadLeft(16, ' ')));
                }
                else if (this.languageExtension == "vb") 
                {
                    code = new CodeSnippetStatement(string.Format("{1}Return Me.{0}", codeMemberField.Name, " ".PadLeft(16, ' ')));
                }

                codeMemberProperty.GetStatements.Add(code);

                if (this.languageExtension == "cs")
                {
                    code = new CodeSnippetStatement(string.Format("{1}this.{0} = value;", codeMemberField.Name, " ".PadLeft(16, ' ')));
                }
                else if (this.languageExtension == "vb")
                {
                    code = new CodeSnippetStatement(string.Format("{1}Me.{0} = value", codeMemberField.Name, " ".PadLeft(16, ' ')));
                }

                codeMemberProperty.SetStatements.Add(code);

                codeMemberProperty.CustomAttributes.Add(new CodeAttributeDeclaration("DataMember"));

                dataContract.Members.Add(codeMemberField);
                dataContract.Members.Add(codeMemberProperty);
                dataContract.TypeAttributes = TypeAttributes.Public;
            }

            namespaces.Add(new CodeNamespaceImport("System"));
            namespaces.Add(new CodeNamespaceImport("System.Runtime.Serialization"));
            namespaces.Add(new CodeNamespaceImport("System.Collections.Generic"));
            foreach (string currentNamespaceName in namespacesHashset) 
            {
                namespaces.Add(new CodeNamespaceImport(currentNamespaceName));
            }

            CodeNamespace codeNamespace = new CodeNamespace();
            codeNamespace.Name = namespaceName;
            codeNamespace.Imports.AddRange(namespaces.Cast<CodeNamespaceImport>().ToArray());
            codeNamespace.Types.Add(dataContract);
            return codeNamespace;
        }

        private string GetFieldNameFromProperty(string language, PropertyInfo property)
        {
            string fieldName;
            if (language == "cs")
            {
                fieldName = property.Name[0].ToString().ToLower() + property.Name.Substring(1);
            }
            else if (language == "vb")
            {
                fieldName = "_" + property.Name[0].ToString().ToLower() + property.Name.Substring(1);
            }
            else 
            {
                throw new NotSupportedException(string.Format("Language extension {0} is not supported.", language));
            }

            return fieldName;
        }
    }
}